﻿using System;
using System.Collections.Generic;
using System.Text;
using System.IO;
using System.Xml.Serialization;

using GolfSigma.Inventory.Extension;

namespace GolfSigma.Inventory.Model
{
    #region Enumerations

    public enum SearchTypeEnum
    {
        SingleDay = 0,
        SingleCourse = 5
    }

    #endregion

    [XmlRoot("Search")]
    [Serializable]
    public class Search : IXmlSerializable
    {
        // Attribute filter id
        private static Guid AttributeFilterId = new Guid("b18c8fa8-4cbc-42c5-8b9f-ed167d77758d");

        #region Properties

        /// <summary>
        /// Id of the search. This MUST be globally unique!
        /// </summary>
        public UInt64 SearchId { get; set; }

        /// <summary>
        /// Date to search. Defaults to DateTime.MinValue
        /// </summary>
        public DateTime Date { get; set; }

        /// <summary>
        /// Optional parameter to search multiple days.
        /// </summary>
        public DateTime? DateTo { get; set; }

        /// <summary>
        /// List of courseId's to search. Serialized into a comma separated list with hyphens removed.
        /// </summary>
        private List<UInt64> _CourseIds;
        public List<UInt64> CourseIds
        {
            get { return _CourseIds; }
            set
            {
                _CourseIds = value;
                _CourseIdsString = null;
            }
        }

        /// <summary>
        /// DateTime of when this tee time was created. (UTC)
        /// </summary>
        public DateTime Created { get; set; }

        /// <summary>
        /// Field used to facilitate lookup of searches based on an id. GUID's are used in GolfSigma
        /// public end-user code (user id's)
        /// </summary>
        public string ExternalId { get; set; }

        /// <summary>
        /// Time of day to search. Defaults to 7:00am. (Will be serialized into number of minutes since mid-night 12:00am or 00:00)
        /// </summary>
        public TimeSpan TimeOfDay { get; set; }

        /// <summary>
        /// Type of search performed. Ex. SingleDay or SingleCourse (Defaults to SingleDay)
        /// </summary>
        public SearchTypeEnum SearchType { get; set; }

        /// <summary>
        /// Number of players to search for (Bitwise)
        /// </summary>
        public PlayerEnum Players { get; set; }

        ///// <summary>
        ///// Interval to group tee-times into. Ex value of "30" would group tee times between 7:00am and 7:29:59am
        ///// </summary>
        //public short Interval { get; set; }

        /// <summary>
        /// List of filters
        /// </summary>
        public List<SearchFilter> Filters { get; set; }

        /// <summary>
        /// List of tee time attributes to filter (auto adds the list to filters)
        /// </summary>
        [XmlIgnore]
        public List<TeeTimeAttributeEnum> Attributes { get; set; }

        /// <summary>
        /// UTC DateTime of when this search was updated
        /// </summary>
        public DateTime? Updated { get; set; }

        /// <summary>
        /// Returns the misc data
        /// </summary>
        public bool ReturnMiscData { get; set; }

        /// <summary>
        /// Return the Fees, added, updated and deleted times
        /// </summary>
        public bool ReturnDetails { get; set; }

        /// <summary>
        /// When a courseId is no longer valid or not yet in the Atlantic service courseId hash we could get an error similiar to the following:
        /// "GolfSigma.Inventory.InventoryException: Value (11908) for parameter CourseId is invalid. Item does not exist or has been deleted."
        /// </summary>
        public bool IgnoreMissingCourseIds { get; set; }

        /// <summary>
        /// Store any data you wish up to 1,000 chars.
        /// </summary>
        public string ApplicationData { get; set; }

        #endregion

        #region Constructor

        /// <summary>
        /// Setup our default values
        /// </summary>
        public Search()
            : base()
        {
            SearchId = 0;
            Created = DateTime.UtcNow;
            SearchType = SearchTypeEnum.SingleDay;
            TimeOfDay = default(TimeSpan);
            Filters = new List<SearchFilter>();
            Players = PlayerEnum.None;
        }

        #endregion

        #region IXmlSerializable Members

        public System.Xml.Schema.XmlSchema GetSchema()
        {
            return null;
        }

        public void ReadXml(System.Xml.XmlReader reader)
        {
            // Check to see if we were provided an id
            string searchId = reader.GetAttribute("id");
            if (!string.IsNullOrEmpty(searchId))
            {
                UInt64 searchIdParsed;
                if (UInt64.TryParse(searchId, out searchIdParsed))
                    SearchId = searchIdParsed;
                else
                    throw new FormatException();
            }

            bool breakRead = false;
            while (breakRead == false)
            {
                reader.Read();
                string localName = reader.LocalName;

                string elementAsString = string.Empty;
                //if (localName != "filters")
                elementAsString = reader.ReadString();

                switch (localName)
                {
                    case "ExternalId":
                        ExternalId = elementAsString;
                        break;

                    case "Date":
                        DateTime timeParsed;
                        if (!DateTime.TryParseExact(
                            elementAsString,
                            Extension.StringExtension.DateTimeFormatShort,
                            System.Globalization.CultureInfo.InvariantCulture,
                            System.Globalization.DateTimeStyles.AssumeLocal | System.Globalization.DateTimeStyles.AllowWhiteSpaces,
                            out timeParsed))
                            throw new FormatException();
                        else
                        {
                            TimeOfDay = timeParsed.TimeOfDay;
                            Date = timeParsed.Date;
                        }
                        break;

                    case "DateTo":
                        DateTime dateToParsed;
                        if (!DateTime.TryParseExact(
                            elementAsString,
                            Extension.StringExtension.DateTimeFormatShort,
                            System.Globalization.CultureInfo.InvariantCulture,
                            System.Globalization.DateTimeStyles.AssumeLocal | System.Globalization.DateTimeStyles.AllowWhiteSpaces,
                            out dateToParsed))
                            throw new FormatException();
                        else
                        {
                            DateTo = dateToParsed.Date;
                        }
                        break;

                    case "Players":
                        int playersParsed = 0;
                        if (!int.TryParse(elementAsString, out playersParsed))
                            throw new FormatException();
                        else
                            Players = (PlayerEnum)playersParsed;
                        if (Players == PlayerEnum.None)
                            throw new FormatException();
                        break;

                    case "Type":
                        try
                        {
                            // Attempt to convert the SearchType into an enum
                            var searchTypeParsed = (SearchTypeEnum)Enum.Parse(typeof(SearchTypeEnum), elementAsString);
                            SearchType = searchTypeParsed;
                        }
                        catch
                        {
                            throw new FormatException();
                        }
                        break;

                    case "CourseIds":
                        // Make sure we have courseIds
                        if (string.IsNullOrEmpty(elementAsString))
                            throw new FormatException();

                        // Create a new list
                        if (CourseIds == null)
                            CourseIds = new List<UInt64>();

                        // Loop through each id and add to the list
                        foreach (var item in elementAsString.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries))
                        {
                            UInt64? courseId = StringExtension.ToUInt64(item);
                            if (!courseId.HasValue)
                                throw new FormatException();

                            // Add to the list
                            CourseIds.Add(courseId.Value);
                        }
                        break;

                    case "Filters":
                        // Create a new list
                        if (Filters == null)
                            Filters = new List<SearchFilter>();

                        XmlSerializer xmlSerializer = new XmlSerializer(typeof(SearchFilter));
                        while (reader.IsStartElement("Filter"))
                        {
                            var filterItem = (SearchFilter)xmlSerializer.Deserialize(reader);

                            if (filterItem != null)
                                Filters.Add(filterItem);
                        }

                        // Find our attribute filters
                        foreach (var filter in Filters)
                        {
                            if (filter.Id != AttributeFilterId)
                                continue;

                            if (!filter.Key.Equals("Attributes", StringComparison.InvariantCultureIgnoreCase))
                                continue;

                            // Create a new list
                            if (Attributes == null)
                                Attributes = new List<TeeTimeAttributeEnum>();

                            foreach (var item in filter.Value.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries))
                            {
                                try
                                {
                                    TeeTimeAttributeEnum attr = (TeeTimeAttributeEnum)Enum.Parse(
                                        typeof(TeeTimeAttributeEnum),
                                        item,
                                        true);

                                    Attributes.Add(attr);
                                }
                                catch { }
                            }
                        }

                        // Remove our attribute filters
                        Filters.RemoveAll(f => f.Id == AttributeFilterId);

                        // Finish reading the filters
                        break;

                    case "Search":
                        // End reading this element
                        reader.ReadEndElement();
                        breakRead = true;
                        break;

                    case "ReturnMiscData":
                        bool returnMisc;
                        if (elementAsString.Trim() == "0")
                            ReturnMiscData = false;
                        else if (elementAsString.Trim() == "1")
                            ReturnMiscData = true;
                        else if (bool.TryParse(elementAsString, out returnMisc))
                            ReturnMiscData = returnMisc;
                        break;

                    case "ReturnDetails":
                        bool returnDetails;
                        if (elementAsString.Trim() == "0")
                            ReturnDetails = false;
                        else if (elementAsString.Trim() == "1")
                            ReturnDetails = true;
                        else if (bool.TryParse(elementAsString, out returnDetails))
                            ReturnDetails = returnDetails;
                        break;

                    case "ApplicationData":
                        this.ApplicationData = elementAsString;
                        break;

                    // Do nothing on any other attribute
                    default: break;
                }
            }
        }

        public void WriteXml(System.Xml.XmlWriter writer)
        {
            writer.WriteAttributeString("id", SearchId.ToString());

            if (!string.IsNullOrEmpty(ExternalId))
                writer.WriteElementString("ExternalId", ExternalId);

            // Show when the date/time of this search is set for
            var searchDateTime = new DateTime(Date.Date.Ticks + TimeOfDay.Ticks);
            writer.WriteElementString("Date", searchDateTime.ToString(Extension.StringExtension.DateTimeFormatShort));

            // Show a few more properties
            writer.WriteElementString("Type", ((int)SearchType).ToString());
            writer.WriteElementString("Players", ((int)Players).ToString());

            if (this.DateTo.HasValue)
                writer.WriteElementString("DateTo", this.DateTo.Value.Date.ToString(Extension.StringExtension.DateTimeFormatShort));

            // Join our courseIds
            if (CourseIds != null && CourseIds.Count > 0)
                writer.WriteElementString("CourseIds", CourseIdsToString());

            // Serialize our filters
            if (Filters != null && Filters.Count > 0)
            {
                var serializer = new XmlSerializer(typeof(SearchFilter));

                writer.WriteStartElement("Filters");

                // Loop through each filter and serialize it!
                foreach (var filter in Filters)
                    serializer.Serialize(writer, filter);

                writer.WriteEndElement();
            }

            writer.WriteElementString("Created", Created.ToString(Extension.StringExtension.DateTimeFormatUTC));

            if (ReturnDetails)
                writer.WriteElementString("ReturnDetails", "1");

            if (ReturnMiscData)
                writer.WriteElementString("ReturnMiscData", "1");

            if (IgnoreMissingCourseIds)
                writer.WriteElementString("IgnoreMissingCourseIds", "1");

            if (!string.IsNullOrEmpty(ApplicationData))
                writer.WriteElementString("ApplicationData", this.ApplicationData);
        }

        #endregion

        #region Post Parameters

        public Dictionary<string, string> GetPostParamters()
        {
            string prefix = "search.";

            // Create our result
            Dictionary<string, string> result = new Dictionary<string, string>();

            result.Add(StringExtension.Prefix("externalId", prefix), ExternalId);
            result.Add(StringExtension.Prefix("date", prefix), new DateTime(Date.Date.Ticks + TimeOfDay.Ticks).ToString(Extension.StringExtension.DateTimeFormatShort));
            result.Add(StringExtension.Prefix("type", prefix), ((int)SearchType).ToString());
            result.Add(StringExtension.Prefix("players", prefix), ((int)Players).ToString());
            result.Add(StringExtension.Prefix("courseids", prefix), CourseIdsToString());

            if (this.DateTo.HasValue)
                result.Add(StringExtension.Prefix("dateTo", prefix), this.DateTo.Value.Date.ToString(Extension.StringExtension.DateTimeFormatShort));

            if (!string.IsNullOrEmpty(ApplicationData))
                result.Add(StringExtension.Prefix("applicationdata", prefix), ApplicationData);

            // Convert our attributes to a filter
            if (Attributes != null && Attributes.Count > 0)
            {
                StringBuilder sb = new StringBuilder();

                // Append the attributes
                foreach (var item in Attributes)
                {
                    sb.AppendFormat(
                        "{0},",
                        ((int)item).ToString()
                        );
                }

                // Create a new list
                if (Filters == null)
                    Filters = new List<SearchFilter>();

                // Add the attribute filter
                Filters.Add(
                    new SearchFilter(
                        AttributeFilterId,
                        "Attributes",
                        sb.ToString(0, sb.Length - 1)
                        )
                    );
            }

            // Append our filters
            if (Filters != null && Filters.Count > 0)
            {
                for (int i = 0; i < Filters.Count; i++)
                {
                    DictionaryExtension.AddRange(
                        result,
                        Filters[i].GetPostParamters(i + 1)
                        );
                }
            }

            if (ReturnDetails)
                result.Add(StringExtension.Prefix("returnDetails", prefix), "1");

            if (ReturnMiscData)
                result.Add(StringExtension.Prefix("returnMiscData", prefix), "1");

            if (IgnoreMissingCourseIds)
                result.Add(StringExtension.Prefix("ignoreMissingCourseIds", prefix), "1");

            // Return the params
            return result;
        }

        #endregion

        #region Helpers

        private string _CourseIdsString;
        /// <summary>
        /// Create a comma seperated list of ids (with the hyphen removed)
        /// </summary>
        /// <returns></returns>
        public string CourseIdsToString()
        {
            if (!string.IsNullOrEmpty(_CourseIdsString))
                return _CourseIdsString;

            if (CourseIds == null || CourseIds.Count < 1)
                return null;

            // Create a new string builder
            var sb = new StringBuilder();

            // Appeach the courseid(s)
            foreach (var item in CourseIds)
            {
                sb.Append(item.ToString());
                sb.Append(",");
            }

            // Set our string
            _CourseIdsString = sb.Length > 1 ? sb.ToString(0, sb.Length - 1) : string.Empty;

            // Return it
            return _CourseIdsString;
        }

        #endregion
    }
}